/******************************************************************************
 * $Workfile: SMTPSender.java $
 * $Revision: 160 $
 * $Author: edaugherty $
 * $Date: 2003-12-23 20:29:38 -0600 (Tue, 23 Dec 2003) $
 *
 ******************************************************************************
 * This program is a 100% Java Email Server.
 ******************************************************************************
 * Copyright (C) 2001, Eric Daugherty
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 ******************************************************************************
 * For current versions and more information, please visit:
 * http://www.ericdaugherty.com/java/mail
 *
 * or contact the author at:
 * java@ericdaugherty.com
 *
 ******************************************************************************
 * This program is based on the CSRMail project written by Calvin Smith.
 * http://crsemail.sourceforge.net/
 *****************************************************************************/

package com.ericdaugherty.mail.server.services.smtp;

//Java imports
import java.io.*;
import java.util.Date;
import java.util.Vector;
import java.util.List;

//Log imports
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

//Local imports
import com.ericdaugherty.mail.server.info.User;
import com.ericdaugherty.mail.server.info.EmailAddress;
import com.ericdaugherty.mail.server.errors.InvalidAddressException;
import com.ericdaugherty.mail.server.errors.NotFoundException;
import com.ericdaugherty.mail.server.services.general.DeliveryService;
import com.ericdaugherty.mail.server.configuration.ConfigurationManager;

/**
 * This class (thread) is responsible for checking the disk for unsent message
 * and delivering them to the proper local address or remote smtp server.
 * <p>
 * There should be only one instance of this thread running in the system at any
 * one time.
 */
public class SMTPSender implements Runnable {

	// ***************************************************************
	// Variables
	// ***************************************************************

	/** Logger */
	private static Log log = LogFactory.getLog(SMTPSender.class);
	static ConfigurationManager configurationManager1 = ConfigurationManager
			.initialize(ConfigurationManager.class.getClassLoader()
					.getResource("").getFile());
	/** The ConfigurationManager */
	private static ConfigurationManager configurationManager = ConfigurationManager
			.getInstance();

	private boolean running = true;

	// ***************************************************************
	// Public Interface
	// ***************************************************************

	/**
	 * The entrypoint for this thread. This method handles the lifecycle of this
	 * thread.
	 */
	public void run() {

		while (running) {

			try {

				log.info("Checking for SMTP messages to deliver");

				File smtpDirectory = new File(
						configurationManager.getMailDirectory()
								+ File.separator + "smtp");

				if (smtpDirectory.exists() && smtpDirectory.isDirectory()) {

					File[] files = smtpDirectory.listFiles();
					int numFiles = files.length;

					for (int index = 0; index < numFiles; index++) {
						try {
							deliver(SMTPMessage.load(files[index]
									.getAbsolutePath()));
						} catch (Throwable throwable) {
							log.error(
									"An error occured attempting to deliver an SMTP Message: "
											+ throwable, throwable);
							// Do nothing else, contine on to the next message.
						}
					}
				}

				// Rest the specified sleep time. If it is greater than 10
				// seconds
				// Wake up every 10 seconds to check to see if the thread is
				// shutting
				// down.
				long sleepTime = configurationManager
						.getDeliveryIntervealMilliseconds();
				if (configurationManager.getDeliveryIntervealMilliseconds() < 10000) {
					Thread.sleep(sleepTime);
				} else {
					long totalSleepTime = sleepTime;
					while (totalSleepTime > 0 && running) {
						if (totalSleepTime > 10000) {
							totalSleepTime -= 10000;
							Thread.sleep(10000);
						} else {
							Thread.sleep(totalSleepTime);
							totalSleepTime = 0;
						}
					}
				}
			} catch (InterruptedException ie) {
				log.error("Sleeping Thread was interrupted.");
			} catch (Throwable throwable) {
				log.error(
						"An error occured attempting to deliver an SMTP Message: "
								+ throwable, throwable);
			}
		}
		log.warn("SMTPSender shut down gracefully.");
	}

	/**
	 * Notifies this thread to stop processing and exit.
	 */
	public void shutdown() {
		log.warn("Attempting to shut down SMTPSender.");
		running = false;
	}

	// ***************************************************************
	// Private Interface
	// ***************************************************************

	/**
	 * This method takes a SMTPMessage and attempts to deliver it. This method
	 * assumes that all the addresses have been validated before, and does not
	 * perform any delivery rules.
	 */
	private void deliver(SMTPMessage message) {

		List toAddresses = message.getToAddresses();
		int numAddress = toAddresses.size();
		Vector failedAddress = new Vector();
		EmailAddress address = null;
		System.out.println("Mail is send to : " + toAddresses);
		// If the next scheduled delivery attempt is still in the future, skip.
		if (message.getScheduledDelivery().getTime() > System
				.currentTimeMillis()) {
			if (log.isDebugEnabled())
				log.debug("Skipping delivery of message "
						+ message.getMessageLocation().getName()
						+ " because the scheduled delivery time is still in the future: "
						+ message.getScheduledDelivery());
			return;
		}

		for (int index = 0; index < numAddress; index++) {
			try {
				address = (EmailAddress) toAddresses.get(index);
				if (log.isDebugEnabled()) {
					log.debug("Attempting to deliver message from: "
							+ message.getFromAddress().getAddress() + " to: "
							+ address);
				}

				DeliveryService deliveryService = DeliveryService
						.getDeliveryService();

				try {
					if (deliveryService.isLocalAddress(address)) {
						deliverLocalMessage(address, message);
					} else {
						deliverRemoteMessage(address, message);
					}
				} catch (NotFoundException e) {
					log.info("Delivery attempted to unknown user: "
							+ address.getAddress());
					// The addressee does not exist. Notify the sender of the
					// error.
					bounceMessage(address, message);
				}

				if (log.isInfoEnabled()) {
					log.info("Delivery complete for message "
							+ message.getMessageLocation() + " to: " + address);
				}
			} catch (Throwable throwable) {
				log.error("Delivery failed for message from: "
						+ message.getFromAddress().getAddress() + " to: "
						+ address + " - " + throwable, throwable);
				failedAddress.addElement(toAddresses.get(index));
			}
		}

		// If all addresses were successful, remove the message from the spool
		if (failedAddress.size() == 0) {
			// Log an error if the delete fails. This will cause the message to
			// get
			// delivered again, but it is too late to roll back the delivery.

			if (message.getMessageLocation() != null
					&& !message.getMessageLocation().delete()) {
				log.error("Error removed SMTP message after delivery!  This message may be redelivered. "
						+ message.getMessageLocation().getName());
			}
		}
		// Update the message with any changes.
		else {

			message.setToAddresses(failedAddress);
			int deliveryAttempts = message.getDeliveryAttempts();

			// If the message is a bounced email, just give up and move it to
			// the failed directory.
			if (message.getFromAddress().getUsername()
					.equalsIgnoreCase("MAILER_DAEMON")) {
				try {
					log.info("Delivery of message from MAILER_DAEMON failed, moving to failed folder.");
					message.moveToFailedFolder();
				} catch (Exception e) {
					log.error("Unable to move failed message to 'failed' folder.");
				}
			}
			// If we have not passed the maximum delivery count, calculate the
			// next delivery time and save the message.
			else if (deliveryAttempts < configurationManager
					.getDeliveryAttemptThreshold()) {
				message.setDeliveryAttempts(deliveryAttempts + 1);

				// Reschedule later, 1 min, 2 min, 4 min, 8 min, ... 2^n
				// Cap delivery interval at 2^10 minutes. (about 17 hours)
				if (deliveryAttempts > 10) {
					deliveryAttempts = 10;
				}
				long offset = (long) Math.pow(2, deliveryAttempts);
				Date schedTime = new Date(System.currentTimeMillis() + offset
						* 60 * 1000);
				message.setScheduledDelivery(schedTime);

				try {
					message.save();
				} catch (Exception exception) {
					log.error(
							"Error updating spooled message for next delivery.  Message may be re-delivered.",
							exception);
				}
			}
			// All delivery attempts failed, bounce message.
			else {
				// Send a bounce message to all failed addresses.
				for (int index = 0; index < failedAddress.size(); index++) {
					try {
						EmailAddress bounce_address = (EmailAddress) (failedAddress
								.elementAt(index));
						bounceMessage(bounce_address, message);
					} catch (Exception e) {
						log.error("Problem bouncing message. "
								+ message.getMessageLocation().getName());
					}
				}

				// Remove the original message.
				if (!message.getMessageLocation().delete()) {
					log.error("Error removed SMTP message after bounce! This message may be re-bounced. "
							+ message.getMessageLocation().getName());
				}
			}
		}
	}

	/**
	 * This method takes a local SMTPMessage and attempts to deliver it.
	 */
	private void deliverLocalMessage(EmailAddress address, SMTPMessage message)
			throws NotFoundException {

		if (log.isDebugEnabled()) {
			log.debug("Delivering Message to local user: "
					+ address.getAddress());
		}

		User user = null;
		// Load the user. If the user doesn't exist, a not found exception will
		// be thrown and the deliver() message will deal with the notification.
		user = configurationManager.getUser(address);
		if (user == null) {
			log.debug("User not found, checking for default delivery options");
			// Check to see if a default delivery mailbox exists, and if so,
			// deliver it.
			// Otherwise, just throw the NotFoundException to bounce the email.
			if (configurationManager.isDefaultUserEnabled()) {
				EmailAddress defaultAddress = configurationManager
						.getDefaultUser();
				// If this throws a NotFoundException, go ahead and let it
				// bounce.
				user = configurationManager.getUser(defaultAddress);
				if (user == null)
					throw new NotFoundException();
				if (log.isDebugEnabled()) {
					log.info("Delivering message addressed to: " + address
							+ " to default user: " + defaultAddress);
				}
			} else {
				throw new NotFoundException(
						"User does not exist and no default delivery options found.");
			}
		}

		// The file to write to.
		File messageFile = null;
		// The output stream to write the message to.
		BufferedWriter out = null;

		try {

			// Get the directory and create a new file.
			File userDirectory = user.getUserDirectory();
			messageFile = userDirectory.createTempFile("pop", ".jmsg",
					userDirectory);

			if (log.isDebugEnabled()) {
				log.debug("Delivering to: " + messageFile.getAbsolutePath());
			}

			// Open the output stream.
			out = new BufferedWriter(new FileWriter(messageFile));

			// Get the data to write.
			List dataLines = message.getDataLines();
			int numDataLines = dataLines.size();

			// Write the X-DeliveredTo: header
			out.write("X-DeliveredTo: " + address.getAddress());
			out.write("\r\n");

			// Write the data.
			for (int index = 0; index < numDataLines; index++) {
				out.write((String) dataLines.get(index));
				out.write("\r\n");
			}
		} catch (IOException ioe) {
			log.error("Error performing local delivery.", ioe);
			if (messageFile != null) {
				// The message was not fully written, so delete it.
				messageFile.delete();
			}
		} finally {
			if (out != null) {
				try {
					// Make sure we close up the output stream.
					out.close();
				} catch (IOException ioe) {
					log.error("Error closing output Stream.", ioe);
				}
			}
		}
	}

	/**
	 * Handles delivery of messages to addresses not handled by this server.
	 */
	private void deliverRemoteMessage(EmailAddress address, SMTPMessage message)
			throws NotFoundException {

		if (log.isDebugEnabled()) {
			log.debug("Delivering Message to remote user: " + address);
		}

		// Delegate this request to the SMTPRemoteSender class.
		new SMTPRemoteSender().sendMessage(address, message);
	}

	private void bounceMessage(EmailAddress address, SMTPMessage message) {

		if (log.isInfoEnabled()) {
			log.info("Bouncing Messsage from "
					+ message.getFromAddress().getAddress() + " to "
					+ address.getAddress());
		}

		SMTPMessage bounceMessage = new SMTPMessage();

		// Set the from address as mailserver@ the first (default) local domain.
		EmailAddress fromAddress = new EmailAddress("MAILER_DAEMON",
				configurationManager.getLocalDomains()[0]);

		bounceMessage.setFromAddress(fromAddress);
		bounceMessage.addToAddress(message.getFromAddress());
		bounceMessage
				.addDataLine("From: Mail Delivery Subsystem <MAILER-DAEMON@"
						+ configurationManager.getLocalDomains()[0] + ">");
		bounceMessage.addDataLine("To: "
				+ message.getFromAddress().getAddress());
		bounceMessage.addDataLine("Subject: Message Delivery Error.");
		bounceMessage.addDataLine("Date: " + new Date().toString()); // TODO:
																		// Improve
																		// date
																		// handling.
		bounceMessage.addDataLine("");
		bounceMessage.addDataLine("Error delivering message to: "
				+ address.getAddress());
		bounceMessage.addDataLine("This message will not be delivered.");
		bounceMessage.addDataLine("");
		bounceMessage.addDataLine("------------------");

		List dataLines = message.getDataLines();

		int numLines = dataLines.size();

		for (int index = 0; index < numLines; index++) {
			bounceMessage.addDataLine((String) dataLines.get(index));
		}
		bounceMessage.addDataLine("");

		// Save this message so it will be delivered.
		try {
			bounceMessage.save();
		} catch (Exception e) {
			log.error("Error storing outgoing 'bounce' email message");
			throw new RuntimeException();
		}
	}

	public static void main(String[] args) throws InvalidAddressException {
		SMTPSender sender = new SMTPSender();
		SMTPMessage message = new SMTPMessage();
		EmailAddress fromAddr = new EmailAddress("wenbindong1984@163.com");
		fromAddr.setShowName("Zhan,James");
		message.setFromAddress(fromAddr);
		message.addToAddress(new EmailAddress("wenbindong1984@163.com"));
		message.addDataLine("just One Test!");
		message.setTitle("just One Test!");
		sender.deliver(message);
		System.out.println("finished.");
	}
}
// EOF